Don't think! Just do it!

종합 IT 기술 정체성 카오스 블로그! 이... 이곳은 어디지?

Flutter/Flutter Project

[소셜차트] 앱 제작기 #8. 글쓰는 페이지

방피터 2022. 11. 5. 12:30

일단은 지금 제작중인 소셜차트라는 앱이 소셜네트워킹 앱이어서 사용자들이 글을 쓸 수 있도록 해야 해. 페이스북이나 트위터처럼 말이지.

이거 말고도 구현해야 할 것들이 굉장히 많지만 글을 쓸 수 있는 환경부터 만드는 게 더미 데이터를 넣으면서 테스트하기 좋을 거 같아서 글쓰는 페이지부터 만들고 있어.

1차적으로 해쉬태그와 url을 자동으로 인식해서 link preview를 보여주는 정도로만 구현중이야.

Screen Write

먼저 textfield에 hashtag와 url을 꾸며주는 건 detactable_text_field라는 flutter package를 사용했고👇

https://pub.dev/packages/detectable_text_field

 

detectable_text_field | Flutter Package

TextField with detection features. You can detect hashtags, at sign, url or anything you want. Helps you develop Twitter like app. Refinement of hashtagable

pub.dev

link preview와 metatag 빼내는 건 any_link_preview라는 flutter package를 사용했어.

https://pub.dev/packages/any_link_preview

 

any_link_preview | Flutter Package

Dart package that helps with preview of any links. Fully customizable and has the ability to render from cached data. Useful for apps where we had chat feature.

pub.dev

 

View는 아래 정도로 간단히 구현 가능하고

  body: SingleChildScrollView(
    child: Container(
      padding: const EdgeInsets.only(
        left: 10,
        right: 10,
        bottom: 50,
      ),
      child: Column(
        children: [
          DetectableTextField(
            keyboardType: TextInputType.multiline,
            detectionRegExp: hashTagUrlRegExp,
            decoration: const InputDecoration(
              hintText: "자신의 인사이트를 공유해보세요!",
              border: InputBorder.none,
            ),
            autofocus: true,
            maxLines: null,
            minLines: null,
            autocorrect: false,
            controller: controller.textController,
            onChanged: (value) {
              controller.userText = value;
            },
          ),
          Obx(
            () => controller.userLink != null
                ? Stack(
                    children: [
                      AnyLinkPreview(
                        displayDirection: UIDirection.uiDirectionHorizontal,
                        link: controller.userLink!,
                        bodyMaxLines: 3,
                        placeholderWidget: const Center(child: Text("로딩중")),
                        errorImage: "Error loding image",
                      ),
                      Positioned(
                        top: 0,
                        right: 0,
                        child: CupertinoButton(
                          minSize: 10,
                          padding: EdgeInsets.all(0),
                          onPressed: () => controller.userLink = null,
                          child: Icon(Icons.cancel, color: Colors.black45),
                        ),
                      )
                    ],
                  )
                : const SizedBox(),
          ),
        ],
      ),
    ),
  ),

Controller에서는 아래와 같이 구현할 수 있어.

class ScreenWriteController extends GetxController
    with GetSingleTickerProviderStateMixin {
  static final _urlRegExp = RegExp(
    "(?!\\n)(?:^|\\s)$urlRegexContent" + "( |\n)",
    multiLine: true,
  );

  var key = GlobalKey<FormState>();
  var textController = TextEditingController();

  final _userText = Rx<String>("");
  final _previewLink = Rxn<String>();

  final _linkData = Rxn<Metadata>();

  final _deletedLink = Rx<List<String>>([]);

  String get userText => _userText.value;
  set userText(String value) => _userText.value = value;

  String? get previewLink => _previewLink.value;
  set previewLink(String? value) => _previewLink.value = value;

  set deletedLink(String value) => _deletedLink.value.add(value);

  @override
  void onInit() {
    // TODO: implement onInit
    super.onInit();
    ever(
      _userText,
      (userText) async {
        Iterable<String> extractedLink =
            extractDetections(userText, _urlRegExp).isEmpty
                ? []
                : extractDetections(userText, _urlRegExp)
                    .where((element) => !_deletedLink.value.contains(element));
        if (extractedLink.isEmpty) return;
        AnyLinkPreview.getMetadata(link: extractedLink.first).then((value) {
          if (value != null) {
            _linkData.value = value;
            previewLink = extractedLink.first;
          } else {
            //do nothing!
          }
        });
      },
    );
  }

  @override
  void onReady() {
    // TODO: implement onReady
    super.onReady();
    //hide main navigation bar
    Get.find<MainNavigatorController>().isBottomTabVisible = false;
  }

  @override
  void onClose() {
    // TODO: implement onClose
    super.onClose();
    //show the main navigation bar again
    Get.find<MainNavigatorController>().isBottomTabVisible = true;
  }
}

 

이제 내용들을 저장해볼건데, 작성자, 작성일 이런 기본적인 것들을 포함해서 link preview 데이터 정도만 추가해주면 될 것 같아. 나머지는 차차 추가해보자. 난 유저가 작성한 글을 insightCard라고 부를거야. 

난 firestore를 사용하고 있어. 그냥 단숨히 document에 string, int... 등을 쓰는 건 잘 되는데.. 또 다른 Map을 넣는 건 어떻게 하는지 모르겠네? ㅋㅋ 뭐 별거 있겠어? 그냥 해보는 거지 ㅋㅋㅋ 일단 model 고고 👇👇

class InsightCardModel {
  InsightCardModel({
    required this.createdAt,
    required this.chartId,
    required this.cardType,
    required this.author,
    required this.userText,
    this.linkPreviewData,
    this.replyCount = 0,
    this.pinCount = 0,
  });
  final Timestamp createdAt; //creation time
  final String chartId;
  final String cardType;
  final DocumentReference author; //userReference
  final String userText;
  final LinkPreviewData? linkPreviewData;
  final int replyCount;
  final int pinCount;

  InsightCardModel.fromJson(Map<String, Object?> json)
      : this(
          createdAt: json['createdAt']! as Timestamp,
          chartId: json['chartId']! as String,
          cardType: json['cardType'] as String,
          author: json['author'] as DocumentReference,
          userText: json['userText'] as String,
          linkPreviewData: json['linkPreviewData'] as LinkPreviewData?,
          replyCount: json['replyCount'] as int,
          pinCount: json['pinCount'] as int,
        );
  Map<String, Object?> toJson() {
    return {
      'createdAt': createdAt,
      'chartId': chartId,
      'cardType': cardType,
      'author': author,
      'userText': userText,
      'linkPreviewData': linkPreviewData,
      'replyCount': replyCount,
      'pinCount': pinCount,
    };
  }
}

class LinkPreviewData {
  LinkPreviewData({
    required this.url,
    this.image,
    this.title,
    this.description,
  });
  final String url;
  final String? image;
  final String? title;
  final String? description;
}

이렇게 모델을 작성하고 firestore에 collection에 add로 문서 업로드를 시도해보니 ㅋㅋ 역시나 에러... Invalid ardument ㅎㅎ 뭐 당연한가? 못알아 먹던 json을 dynamic 속에 넣었다고 알아먹을리가 없으니까 ㅎㅎ

에러!

중첩되어 사용될 클래스에도 fromJson과 toJson을 추가해주고 그걸 메인 모델 클래스에서 사용하도록 하자구.

class InsightCardModel {
  InsightCardModel({
    required this.createdAt,
    required this.chartId,
    required this.cardType,
    required this.author,
    required this.userText,
    this.linkPreviewData,
    this.replyCount = 0,
    this.pinCount = 0,
  });
  final Timestamp createdAt; //creation time
  final String chartId;
  final String cardType;
  final DocumentReference author; //userReference
  final String userText;
  final LinkPreviewData? linkPreviewData;
  final int replyCount;
  final int pinCount;

  InsightCardModel.fromJson(Map<String, dynamic?> json)
      : this(
          createdAt: json['createdAt']! as Timestamp,
          chartId: json['chartId']! as String,
          cardType: json['cardType'] as String,
          author: json['author'] as DocumentReference,
          userText: json['userText'] as String,
          //여기!!!!!!
          linkPreviewData: LinkPreviewData?.fromJson(json['linkPreviewData']),
          replyCount: json['replyCount'] as int,
          pinCount: json['pinCount'] as int,
        );
  Map<String, dynamic?> toJson() {
    return {
      'createdAt': createdAt,
      'chartId': chartId,
      'cardType': cardType,
      'author': author,
      'userText': userText,
      //여기!!!!!!
      'linkPreviewData': linkPreviewData?.toJson(),
      'replyCount': replyCount,
      'pinCount': pinCount,
    };
  }
}

class LinkPreviewData {
  LinkPreviewData({
    this.url,
    this.image,
    this.title,
    this.description,
  });
  final String? url;
  final String? image;
  final String? title;
  final String? description;

  //추가!!!!!!
  LinkPreviewData.fromJson(Map<String, dynamic> json)
      : this(
            description: json['description'],
            image: json['image'],
            title: json['title'],
            url: json['url']);
  //추가!!!!!!
  Map<String, dynamic> toJson() => {
        "url": url,
        "image": image,
        "title": title,
        "description": description,
      };
}

그리고 나서 테스트를 해보면 👇

잘 동작한다.

이제 더미 데이터를 몇십개 만들고 listview로 만들어 볼 예정. ㅎㅎ 더디긴 해도 진도가 나가긴 하네 ㅎㅎㅎ 셀프 화이팅이다 ㅋㅋㅋㅋㅋㅋ 안녕!

반응형