반응형

고성과 팀을 원한다면, 85%만 요구하라

 

최대의 성과(maximum results)를 원한다면 최대의 노력(maximum effort)이 필요하다. 고통이 없이는 결실도 없다.

 

그러나 이는 고루한 사고방식에 불과하다.

 

과도한 노력은 성과로 이어지지 않을 뿐만 아니라 탈진과 유독한 조직문화의 악순환을 이끈다

 

"최대(maximum)가 아니라, 최적의(optimal) 노력이 성과를 이끈다."

 

그래서 85% 규칙이 등장한다. 최대의 성과를 얻기 위해선 의도적으로 투입되는 노력을 줄여야 한다.

 

예컨대, 단거리 달리기 선수는 처음부터 100% 수준으로 속도를 올릴 수 있다. 하지만 경기 내내 속도가 떨어지게 된다. 

 

올림픽 금메달리스트인 Carl Lewis는 고통의 순간까지 본인을 몰아 붙이기 보다는 적정한 수준에서 휴식을 취하는 게 중요하다고 역설한다. 훈련과 노력도 정도껏 해야 실전에서 힘을 발휘한다. 

 

달리는 순간에도 턱과 얼굴, 눈에는 힘을 빼 줘야 한다. 이를 악물고 뛰면 이내 이 긴장감이 턱과 목으로 전해지고, 결국은 몸과 다리에 부담을 주게 된다.

 

"오늘은 그만(done for the day)… 시간 정하기"

 

가능하다면 관리자들은 근무 종료 시간을 정하는 게 도움이 된다. 적정 근무 시간이 정해지지 않으면, 피로감에 의사결정이 지체되고 전반적인 근무 성과가 떨어지고, 직원들과 관계에도 영향을 미친다.

 

탁월한 관리자는 직원들이 근무 시간을 적절하게 유지할 수 있도록 독려한다. 어떤 직원은 다른 동료들이 퇴근한 이후에도 뒤늦게까지 남아 일하는 것이 자신의 노력을 보여주고 인정받는 거라 생각한다. 

 

그러나 절대적인 긴급 상황이 아닌 경우에는 늦게 일하는 것을 권장하는 건 바람직하지 않다. 다음날 아침에 신선하고 활기찬 모습으로 출근할 수 있게 귀가를 유도하는 게 더 효과적이다.

 

"한계체감의 법칙: 최대한의 Capacity 보다 조금 덜 일하기"

 

사람들은 최대한의 노력이 최대한의 성과로 이어질 거라 착각한다.

 

그러나 노력과 성과는 비례하지 않는다. 일정 수준의 노력과 근무시간을 넘기면, 성과는 떨어지기 시작한다.

 

 

고성과 팀을 원한다면, 85%만 요구하라 - 경영전문블로그 Innovator

반응형

Flutter 에서 구글맵을 사용할때 2가지 패키지를 추가해서 사용합니다.

 

google_maps_flutter: ^2.2.3
geocoding: ^2.0.5

 

google map 사용방법은 google cloud service에서 api key를 발급 받고 사용해야합니다.

클라우드 설정이 끝나고 Goecoding을 사용하는 방법은 매우 간단합니다.

 

Geo Coding

geocoding, GeoCode, geocoder등의 용어는  주소를 위도, 경도로 변환하거나 위도,경도로 주소를 넘겨받는 기능을 의미하는 표현으로 많이 사용됩니다.

 

 

주소로  위도, 경도를 받아올때

 

List<Location> locations = await locationFromAddress(address);
Location location = locations[0];
print("locations : ${locations}");

 

위도 경도로, 주소 목록을 받아올때

_placemarks = await placemarkFromCoordinates(location.latitude, location.longitude);
print("placemarks : ${_placemarks}");

 

google geocoding 으로 위도 경도를 입력해서 주소 결과물의 형태는 다음과 같습니다.

2023-05-11 17:02:01.282 1519-1584/com.example I/flutter: placemarks : [
Name: 131, 
          Street: 대한민국 경기도 수원시 팔달구 팔달산로 131, 
          ISO Country Code: KR, 
          Country: 대한민국, 
          Postal code: 16440, 
          Administrative area: 경기도, 
          Subadministrative area: ,
          Locality: 수원시,
          Sublocality: 팔달구,
          Thoroughfare: ,
          Subthoroughfare: 131,       
Name: 16440, 
          Street: 대한민국 경기도 수원시 팔달구, 
          ISO Country Code: KR, 
          Country: 대한민국, 
          Postal code: 16440, 
          Administrative area: 경기도, 
          Subadministrative area: ,
          Locality: 수원시,
          Sublocality: 팔달구,
          Thoroughfare: ,
          Subthoroughfare: ,       
Name: 화서1동, 
          Street: 대한민국 경기도 수원시 팔달구 화서1동, 
          ISO Country Code: KR, 
          Country: 대한민국, 
          Postal code: , 
          Administrative area: 경기도, 
          Subadministrative area: ,
          Locality: 수원시,
          Sublocality: 팔달구,
          :

 

반응형

 

어플리케이션 개발시에 google map을 사용해야 하는 경우 종종 있는데요.

google map 사용시 한가지 불편한 부분에 대한 팁입니다.

 

pubspec.yaml

google_maps_flutter: ^2.2.3

 

GoogleMap(
  mapType: MapType.normal,
  mapToolbarEnabled: false,
  zoomControlsEnabled: false,
  myLocationEnabled: true,
  trafficEnabled: false,
  gestureRecognizers:{
    Factory<OneSequenceGestureRecognizer>(() => EagerGestureRecognizer())
  },
  markers: {Marker(markerId: MarkerId("위치"),position: controller.location.toLatLng())},
  onMapCreated:  (GoogleMapController mapController){
    controller.mapController.complete(mapController);
    },

  onTap: (loc) async {
    controller.location = MapLocation.fromLatLng(loc);
    dprint("loc: (${controller.latitude}, ${controller.longitude})");
    final mc = await controller.mapController.future;
    mc.animateCamera(CameraUpdate.newCameraPosition(CameraPosition(target: loc, zoom: await mc.getZoomLevel())));
    setState(() {});
    WMapUtils.getAddress(loc.latitude, loc.longitude).then((value){
      setState(() {
        controller.addressEditController.text = value;
      });
    });
  },
  initialCameraPosition: CameraPosition(target: controller.location.toLatLng(),zoom: 15),

)),

 

googlemap이 SingleChildScrollView 의 child로 들어갈 경우, pinch zoom, panning 등의 gesture 동작이 Scroller에 우선되어서 googlemap에 전달이 안되는 불상사가 있습니다.

그래서 아래와 같이 GoogleMap에 gestureRecognizers 에 설정을 EagerGestureRecognizer 로 세팅해서 처리하면 google map이 우선되어 동작하게 됩니다.

 

gestureRecognizers:{
  Factory<OneSequenceGestureRecognizer>(() => EagerGestureRecognizer())
},

 

 

해피 코딩!

반응형

Camera를 활용한 기능을 만들고 싶을때가 많은데요.

그럴때를 대비해서 하나 예제를 만들어 두었습니다.

 

먼저 pubspec.yaml에 camera pacakge를 import 합니다.

pubspec.yaml

dependencies:
  flutter:
    sdk: flutter
  camera:
  get: ^4.6.5

 

아래 코드를 프로젝트에 추가합니다. 

getx 기반으로 작성된 코드입니다.

 

import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';

class TakePhotoPage extends StatefulWidget{



  @override
  State<TakePhotoPage> createState() => _TakePhotoPageState();

  const TakePhotoPage({super.key});
}

class _TakePhotoPageState extends State<TakePhotoPage> {
  late final List<CameraDescription>cameras ;

  CameraController? _controller;
  late var _initializeControllerFuture;

  @override
  void initState() {
    super.initState();
    availableCamera();

  }
  void availableCamera() async{
    cameras = await availableCameras();
    if(cameras.isEmpty) return;

    setState(() {
      _controller = CameraController(cameras[0], ResolutionPreset.high);
      _initializeControllerFuture = _controller!.initialize();
    });
  }


  @override
  void dispose() {
    _controller?.dispose();
    super.dispose();

  }

  @override
  Widget build(BuildContext context) {

    return Scaffold(
      body : SizedBox(
        width: MediaQuery.of(context).size.width,
        height: MediaQuery.of(context).size.height,
        child: _controller == null?Container(): FutureBuilder<void>(
            future: _initializeControllerFuture,
            builder:(context, snapshot){
              if(snapshot.connectionState == ConnectionState.done){
                return CameraPreview(_controller!);
              }else{
                return const Center(child: CircularProgressIndicator());
              }
            }
        ),
      ),
      floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
      floatingActionButton: FloatingActionButton(
        child:const Icon(Icons.camera_alt),
        onPressed: () async{
          final image = await _controller?.takePicture();
          if(image == null) return;
          // TODO: 사진 촬령후 image를 가지고 하고자 하는 작업 진행
          // ex)
          //   Get.back()  => 이전 화면으로 이동
          //   Get.off( NewPage()); // NewPage()로 이동

        },),
    );
  }
}

 

 

해피 코딩 !!

반응형

요즘 Flutter 로 ble device를 다루는 작업을 하고 있습니다.

 

아시다 시피 BLE 장치들은 기존 BT classic과 다르게 broadcasting 기능을 제공하고 있어서 S/W로직을 기존 BT classic과 동일하게 가져가기에는 무리가 있습니다.

BLE 장치를 어떤 목적으로 어떻게 사용할것인가를 잘 구상 해야합니다.

구성 하고자 하는 장치를 미미(mimi)라고 합시다.

1.  미미에 탑재한 센서에서 읽은 값을 broadcasting 만 목적으로 한다. 

 => BLE advertizement

2. 미미에 탑재한 센서에서 읽은 값을 연결되었을때만 데이타를 읽어갈 수 있도록 한다.

 => BLE service characteristics

3. 미미에 탑제된 여러 장치들을 제어하려고 한다.

 => connect /read /write

보통 목적은 3가지 형태가 될것 같습니다.

 

1번의 경우는 BLE advertizement를 이용하는 방법을 사용합니다.

- connection 없이 모단말(ble에선 central이라 부름)에서 scanning을 할때 scanning 한 데이타를 활용합니다.

- 이를 위헤서 미미(ble장치)에서도 당연히 advertizement 에 데이타를 설정 해줘야합니다.

- spec이나 가이드 상으로는 advertizementdata에 service data 항목이 있어서 service id와 data를 설정할 수 있습니다.

- 그러나 이렇게 사용할 수 없다면, manufacturer data를 활용하는 방법으로 할 수 도 있습니다.

- 여러 단말에서 동시에 읽어갈 수 있습니다.

 

2번의 경우는 BLE service의 characteristic의 값을 읽는 방식을 취합니다.

- scan후에 scan된 기기에 connect하고 나서 data를 읽어가는 방식입니다.

- 1번의 경우에는 scan만으로 정보를 읽어갈 수 있지만, 보안상 취약합니다. 모든 기기가 값을 다 볼 수 있기 때문이죠.

- 이런 connection 방식은 1:1통신이기 때문에, 연결되고 나면 다른 기기에서는 scan이 안됩니다.

- 때문에 connect후에 data를 읽고 바로 연결을 끊어야 합니다.

 

3번의 경우는 2번과 유사한데 다만 scanning 과 분리해서 connect후 characteristic에 read/write를 하는 방식으로 처리합니다.

- serial 방식( classic 방식) 과 유사한 요청사항이죠.

- 하지만 serical에서는 연결후 data를 보내고 response를 받는 것만 고려 했다면, BLE에서는 특정 characteristic정하고 해당 characteristic 값에 data를 wrtie하고 notifiy 를 설정하고 read를 하는 방식이라, 단순한 기능 구현에는 번거로움이 있지만,

미미(mini, ble 장치)가 복잡하다면 각 기능 제어를 분리해서 사용할 수 있기 때문에 좋습니다.

 

 

device scanning 예제 입니다.

package 는 flutter_blue_plus 를 사용했습니다.

 

pubspec.yaml


  flutter_blue_plus: ^1.3.1

 

AndroidManifest.xml

project/android/app/src/main/AndroidManifest.xml

project/android/app/src/profile/AndroidManifest.xml

 

    <uses-permission android:name="android.permission.BLUETOOTH" />
    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
    <uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>

 

Android platform 에서 GPS가 켜져있지 않으면, Bt device scan이 안됩니다.

그냥 BT 기기를 사용하는 입장에서는 GPS 없이도 device 인식이 가능한 것이 상식적일 것 같은데, 실제로 android에서는 그렇지 못합니다.

https://stackoverflow.com/questions/33045581/location-needs-to-be-enabled-for-bluetooth-low-energy-scanning-on-android-6-0

 

Location needs to be enabled for Bluetooth Low Energy Scanning on Android 6.0

After upgrading to Android version 6.0 Bluetooth Low Energy (BLE) scanning will only work if Location services are enabled on the device. See here for reference: Bluetooth Low Energy startScan on A...

stackoverflow.com

이럴경우  android:usesPermissionFlags="neverForLocation" 를 이용해서 scan이 가능하도록 설정 할 수 있습니다.

이 Flag는 상당히 미묘한데요.

위에 stackover flow 를 읽어보면, 위치정보를 기반으로 becon ble 등을 인식 하는 서비를 위해 GPS가 있을떄만 scan 이 되도록 한것 같은데, (그 외에 사생활 보안 등등의 이슈?? 도 있겠죠?)

서비스를 운영하는 입장에서 누군가가 GPS가 꺼져 있으니 동작 안합니다.(정확히는 BT가 scan 안됩니다). 라고 한다면, 답하기가 좀 곤란합니다. 

아무튼 플랫폼 정책상 GSP 꺼져있을때 scan이 안되도록 한 이유는 있겠지만, 우리는 항상 잘 BT 기기를 찾아야 하니 neverForLocation 같은 옵션을 사용하게 됩니다.

    <uses-permission android:name="android.permission.BLUETOOTH" />
    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
    <uses-permission android:name="android.permission.BLUETOOTH_SCAN" 
                     android:usesPermissionFlags="neverForLocation" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>

 

 

 

Sample code

import 'LabBleGenericController.dart';

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';



class LabBleDeviceGenericPage extends StatefulWidget{
  const LabBleDeviceGenericPage({super.key});

  @override
  State<LabBleDeviceGenericPage> createState() => _LabBleDeviceGenericPageState();
}


class _LabBleDeviceGenericPageState extends State<LabBleDeviceGenericPage> {
  StringBuffer data =StringBuffer();

  LabBleGenericController controller= LabBleGenericController();

  bool connectAll = true;
  bool isScanning = false;

  startScan()async{
    print("start scan!!!!!!!");
    isScanning = true;
    controller.scan(
        onScan:(val)=>setState(() { }),
        onScanDone:(){
          setState(() { });
          if(isScanning) {
            startScan();
          }
        });
  }
  stopScan() async{
    isScanning = false;
    await controller.stop();
    setState(() {
    });
  }


  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title:Text("BleDevice")),
      body:Container(
        child: Column(children:[
          TextField(
            controller: controller.filterEdit,
            decoration: InputDecoration(prefix: Text("Filter:"),border: OutlineInputBorder()),
          ),
          SizedBox(height: 20,),
          Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              ElevatedButton(
                  onPressed: (){
                    setState(() { data.clear(); });
                    if(isScanning){
                      stopScan();
                    }else{
                      startScan();
                    }
                  },
                  child: Text(isScanning?"Stop":"Scan")),
              ElevatedButton(
                  onPressed: (){
                    connectAll?controller.connectAll(): controller.disconnectAll();
                    connectAll =! connectAll;
                  },
                  child: Text(connectAll?"connect all":"disconnect all")),
              ElevatedButton(
                  onPressed: (){
                      setState(() { controller.updateDevices();});
                  },
                  child: Text("update")),
            ],
          ),

          SizedBox(height: 50,),
          

          Expanded(
            child: SingleChildScrollView(
              child: Column(
                children: [
                  SizedBox(
                      child: Text(controller.devicesStatus)),
                  SizedBox(
                      child: Text(controller.getResult()))
                ],
              ),
            ),
          ),

         

        ]),
      ),
    );
  }

  @override
  void dispose() {
    controller.close();
    super.dispose();
  }
}

 

controller

import 'dart:async';

import 'package:flutter/cupertino.dart';
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
import 'package:get/get.dart';



class LabBleGenericController extends GetxController{

  StreamSubscription? sub;

  StringBuffer resBuffer =StringBuffer();
  final filterEdit =TextEditingController();
  
  final devices = <BluetoothDevice>[];

  LabBleGenericController(){
    filterEdit.text = "arbot";
  }


  @override
  void onClose() {
    print("LabBleGenericController:onClose()");
    sub?.cancel();
    super.onClose();
  }

  Future connectAll() async{
    for(final d in devices){
      d.connect();
    }
    updateDevices();
  }


  closeAll() async{


    for(final d in await FlutterBluePlus.instance.connectedDevices){
      await d.disconnect();
    }
    devices.clear();
  }

  disconnectAll() async{

    for(final d in devices){
      await d.disconnect();
    }
    updateDevices();
  }

  stop()async{
    await sub?.cancel();
    await FlutterBluePlus.instance.stopScan();
    sub= null;
  }

  scan({Function(ScanResult)? onScan,Function()? onScanDone}) async{

    await updateDevices();
    sub?.cancel();
    resBuffer.clear();

    await FlutterBluePlus.instance.stopScan();
    await closeAll();

    sub = FlutterBluePlus.instance.scan(timeout: Duration(seconds: 10)).listen((ScanResult result) {
      final localName = result.advertisementData.localName;
      if(!localName.toLowerCase().contains(filterEdit.text)) return;
      
      devices.add(result.device);
      
      print("localname:$localName");
      resBuffer.writeln(localName);
      resBuffer.writeln("[advertisementData]");
      resBuffer.writeln("manufacturerData:");

      int i =0;
      for(final k in result.advertisementData.manufacturerData.keys){
        final data = result.advertisementData.manufacturerData[k];
        resBuffer.write("\nkey($k):");
        if(data == null) continue;
        print("idx[$i]:${data.toString()}");
        print("str: ${String.fromCharCodes(data)}");
        resBuffer.writeln(" ${data.toString()}");
        i++;
      }
      resBuffer.writeln("[serviceUuids:${result.advertisementData.serviceUuids.length}]");
      i=0;
      for(final d in result.advertisementData.serviceUuids){
        resBuffer.writeln("[$i]: $d");
        i++;
      }
      resBuffer.writeln("serviceData:");
      i=0;
      for(final k in result.advertisementData.serviceData.keys){
        resBuffer.write("\n$k:");
        final data = result.advertisementData.serviceData[k];
        if(data == null) continue;
        print("idx[$i]:${data.toString()}");
        print("str: ${String.fromCharCodes(data)}");
        resBuffer.write("$data");
        i++;
      }
      print("buffer[${resBuffer.length}]:${resBuffer.toString()}");
      resBuffer.writeln("----------");
      onScan?.call(result);
    },
    onDone: (){
      sub?.cancel();
      onScanDone?.call();
      return ;
    });
  }

  String getResult(){
    return resBuffer.toString();
  }

  final _devicesStatus = "".obs;
  get devicesStatus => _devicesStatus.value;

  updateDevices() async{
    final buff = StringBuffer();
    for(final d in devices){
      buff.writeln("[${d.name}]: ${(await d.state.first).toString()}");
    }
    _devicesStatus.value =  buff.toString();
  }

  close(){
    sub?.cancel();
  }
}

 

해피 코딩!!

반응형

Flutter 앱을 chrome 으로 실행하려고 할때 httpRequest 의 경우 에러가 발생합니다.

flutter sdk에서 막혀있어서 그러는데요.

아래와 같이 조치를 취해서 실행해볼 수 있습니다.

(오래전부터 있었던 사항인데, 저도 종종 sdk update 하고 나서 잊어버리고 실행안되서 해매는 부분이라 적어봤습니다.)

 

 

1-  flutter\bin\cache 폴더에서  flutter_tools.stamp 을 삭제합니다.

2-  flutter\packages\flutter_tools\lib\src\web 에서  chrome.dart 를 에디터로 엽니다.

3-  '--disable-extensions' 을 찾습니다. 

4-  다음 줄에 '--disable-web-security' 을 추가 합니다.

 

이렇게 하고 테스트를 해보세요.

 

추가로 잡설을 하자면, CORS 문제인데요.

CORS (cross-origin resource sharing) 즉 WEB의 기본인 SOP(Same Origin Policy) 와 반대되는 상황입니다.

두 URL의 구성 요소 중 Scheme, Host, Port, 이 3가지가 동일해야 같은 출처 (같은 origin) 으로 판단되기 때문에 이를 해결하기 위해서 기본적으로 서버에서 CORS 를 허가해줘야 합니다.

예를 들면 아래와 같은 방식이죠.

app.use(function(req, res, next) {
  res.header("Access-Control-Allow-Origin", "*");
  res.header("Access-Control-Allow-Methods", "GET,PUT,PATCH,POST,DELETE");
  res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
  next();
});

 

그러나 CORS 를 허가 해준다는 의미는 외부에서 접근을 할수 있는 방법을 하나 열어준것이기 때문에 서버입장에서는 더 보안 리스크를 안고 가는 것이 되겠죠?

 

flutter의 테스트를 chrome으로 해보는 입장이라면 그냥 위와 같이 flutter sdk에서 security 옵션을 정리하는 것으로 하는 것이 편리할 것입니다. 

 

 

 

 

!해피 코딩.!!

 

반응형

(2023년1월28일)

어제 시간이 좀 나서 flutter 3.2 버전으로 개발 진행해오던 프로젝트를 3.7로 업그레이드 해봤습니다.

아.. 뭔가 달라진게 있는지 custom 으로 작성한 theme 쪽에서 deprecated 된 것이 보이고,

화면 전환 이펙트( transit effect) 시 약간 부자연스런 것들이 보여서 여러번 시도하면서 로그들을 봤는데요.

현재 사용하고 있는 프로젝트에서 google map을 사용하고 있는데, 화면전환시 google map이 1~2 프레임(frame) 늦게 사라지는 것을 발견 했네요.

ㅠ_ㅠ 

 

3.7에서 다른 개선된 부분들이 많이 보이는 것 같던데, 저는 google map 때문에 당분간 3.2를 써야 할것 같습니다. 

 

(2023년 11월 14일)

구글맵의 심각한 버그입니다.

3.7에서 여전히 버그가 개선되지 않았습니다.

https://github.com/flutter/flutter/issues/118498

 

해당 문제는 Android 에서만 발생하는 것 같네요.

 

flutter 화면 전환시 google map 화면이 사라지지 않고 1 frame 남아있는 문제

 

 

 

해결책!!

해결책을 찾긴 했습니다. 

 Workaround !!!!

완전 버그 회피 코드이긴 한데, 당분간 이렇게 사용 해야 할것 같네요.

 

https://spikez.tistory.com/490

 

[Flutter] flutter 3.7 google map 버그 (frame이 남아있는 문제) - 2 (해결)

안녕하세요. Flutter Bug에 대한 해결책을 정리해볼까 합니다. https://spikez.tistory.com/456 [Flutter] flutter 3.7 google map 버그 (frame이 남아있는 문제) (2023년1월28일) 어제 시간이 좀 나서 flutter 3.2 버전으로 개

spikez.tistory.com

 

오랜시간 기다렸지만, 해결될 기미가 보이지 않아서 어쩔수 없이 회피 코드를 찾아서 작성하게 되었습니다.

 

!!! 해피 코딩!!!

 

 

#플루터 #버그 #3.7 #망했다. # 써보고 # 싶었는데... #유유 

반응형

[1.이미지(Image)]

[2.컬러(Color)]

[3.텍스트스타일(TextStyle]

 

3. 텍스트 스타일(TextStyle)을 enum으로 관리하는 방법

Text("hello", style: ResTextStyle.t1.bold())

 

 

TextStyle은 flutter ui에서 상당히 많이 사용됩니다.

당연히도 화면에 글지로 표시하여 사용자에게 정보나 의미를 전달해야 되기 때문이죠.(당연한 말이죠? ㅎㅎ)

제가 flutter를 사용하면서 TextStyle을 매번 지정하는 것은 상당히 번거로운 일이었습니다.

코드의 라인수도 늘어날 뿐 아니라 보기도 다른 코드들과 섞이면 흔히 말하는 시인성(코드의 리더빌리티, Readability) 가 떨어지게 되서 불편합니다.

또 코딩하다 text style을 지정할때 잠깐 머뭇거리면서 끈김 현상이 발생합니다(제 머리가요..)

 

TextField(
        controller: controller,
        readOnly: readOnly??false,
        minLines: 1,
        maxLines: 7,
        decoration: InputDecoration(
            contentPadding: EdgeInsets.symmetric(vertical: W(75)),
            filled: true, fillColor: Color.fromRGBO(100,120,100,1.0),
            border: OutlineInputBorder(
                borderRadius: BorderRadius.circular(W(60)), borderSide: BorderSide.none),
            prefixIcon: icon,
            hintText: hintText,
            hintStyle: TextStyle(color: Color.fromRGBO(25,20,25,1), fontSize: 65, fontWeight: FontWeight.w500)),
    );

저는 이런점들을 좀 줄여보고자 몇가지 고민을 했었는데요.

일단 어플리케이션에 사용하는 폰트크기를 미리 정의하는 것이었습니다.

 

class ResFonts{
static cosnt int t1 = 65;
static cosnt int t2 = 58;
static cosnt int t3 = 48;
static const int m1= 24;
static const int m2= 18; 
static const int m3= 16; 
static const int m4= 10; 

 

이런 식으로 정의해서 사용했죠.

또 getTextStyle() 같은 함수를 만들어 사용해보기도 했는데요. 오히려 코딩시에 더 거슬리더군요.

 

최근 enum을 이용해서 간단 textStyle을 만들었는데 의외로 쓸만해서 계속 사용하게 되었습니다.

소개해드리겠습니다.

 

TextStyle 관리하기

제가 사용하는 textstyle 패턴은 아래와 같습니다.

텍스트와 텍스트스타일을 함께 사용하게 되는데 enum을 사용해서 쓰던 ResImage 와 유사하죠.?

Text("hello", style: ResTextStyle.t1.defStyle)

이렇게 사용하면서 상당히 코드의 시인성도 좋아지고 타이핑도 간결해져서 좋더군요.

 

어떻게 만들었는지 한번 보겠습니다.

 

enum을 정의하는 형식은 아래와 같습니다. 폰트의 크기와 기본 weight을 설정하는 형태이죠.

enum ResTextStyle{
  /** fontsize:68 */
  t1(FontWeight.bold, 68),
  /** fontsize:62 */
  t2(FontWeight.bold, 62),
  /** fontsize:58 */
  m1(FontWeight.normal, 58),//58
  /** fontsize:48 */
  m2(FontWeight.normal, 48),

이렇게 하기 위해서는 생성자(constructor)를 다음과 같이 만들어줘야 합니다.

 

final FontWeight fontWeight;
final double fontSize;

const ResTextStyle(this.fontWeight, this.fontSize);

fontWeight와 fontSize를 가지고 생성할 수 있도록 했죠.

다음으로  textStyle을 생성해주는 getter를 하나 만듭니다.

TextStyle get defStyle => TextStyle( fontWeight: fontWeight, fontSize: W(fontSize));

이렇게 하면 위에서 봤던 ResTextStyle.t1.defStyle 이런 방식의 사용이 가능해집니다.

 

저는 처음에 여기까지 해놓고 나서 사용하기 시작했는데, 불편한점들이 몇가지 있었습니다.

- 일단 색상 선택이 불가능하다

- 같은 폰트 크기인데 bold와 normal을 구분하고 싶다.

이런 문제들이 있어서 추가로 getter를 만들려다 함수를 만들게 되었는데,

bold(), normal() 이라는 함수를 추가로 만들게 되었는데, 이러고 나니 상당히 편리한 형태로 사용할수 있고 확장도 가능해졌습니다.

 

TextStyle normal({Color? color, TextOverflow? overflow}) => TextStyle(color: color??ResColors.grayScaleBlack, fontWeight: FontWeight.normal, fontSize: fontSize, overflow: overflow);
TextStyle bold({Color? color, TextOverflow? overflow}) => TextStyle(color: color??ResColors.grayScaleBlack, fontWeight: FontWeight.bold, fontSize:fontSize, overflow: overflow);

실제 사용시에는 다음과 같습니다.

Text("hello", style: ResTextStyle.t1.bold(color:ResColor.gray100)

이런식으로 font크기 + weight + color 가 가능한 형태가 되더군요.

IDE에서 쉽게 검색도 가능한 형태로 말이죠.

 

최종본


enum ResTextStyle{
  /** fontsize:68 */
  t1(FontWeight.bold, 68),
  /** fontsize:62 */
  t2(FontWeight.bold, 62),
  /** fontsize:58 */
  m1(FontWeight.normal, 58),//58
  /** fontsize:48 */
  m2(FontWeight.normal, 48),

  h1(FontWeight.normal, 65), //by ayeong
  ;

  final FontWeight fontWeight;
  final double fontSize;

  const ResTextStyle(this.fontWeight, this.fontSize);

  TextStyle get defStyle => TextStyle( fontWeight: fontWeight, fontSize: fontSize);
  TextStyle normal({Color? color, TextOverflow? overflow}) => TextStyle(color: color??ResColors.grayScaleBlack, fontWeight: FontWeight.normal, fontSize: fontSize, overflow: overflow);
  TextStyle bold({Color? color, TextOverflow? overflow}) => TextStyle(color: color??ResColors.grayScaleBlack, fontWeight: FontWeight.bold, fontSize: fontSize, overflow: overflow);

}

 

이렇게 해서 제가 사용하는 flutter에서 enum을 이용한 resource 관리 방법 3가지 소개해드렸습니다.

 

!! 해피 코딩!!

 

[1.이미지(Image)]

[2.컬러(Color)]

[3.텍스트스타일(TextStyle]

+ Recent posts